package  de.cube3d.blitwit.render
{
	import flash.display.BitmapData;
	import flash.geom.Point;
	import flash.geom.Rectangle;
	import flash.utils.Dictionary;
	import flash.utils.getTimer;
	import de.cube3d.blitwit.space.Space;
	import de.cube3d.blitwit.space.SpaceObject;
	import de.cube3d.blitwit.test.Main;
	
	/**
	 * A scene consisting of spaces that are being drawn sequencially on a bitmap of predefined size.
	 * 
	 */
	public class BlitSpriteScene 
	{
		protected var _bm:BitmapData;
		protected var _dirtPool:Vector.<Rectangle> = new Vector.<Rectangle>(); // oink oink...
		protected var _dirtListUse:int = 0; // the part of the pool that is really dirty
		protected const CELL_SIZE:int = 64;
		protected const MAX_SIZE:int = 4096;
		protected var _dirtmarker:int = 1;
		protected var _dirtyScreenArea:Vector.<int> = new Vector.<int>(MAX_SIZE / CELL_SIZE * MAX_SIZE / CELL_SIZE, true);
		protected var _isTransparent:Boolean = false;
		
		protected var _sceneSpaces:Vector.<Space> = new Vector.<Space>();
		
		/**
		 * The region that the scene is displaying. Despite the floating points in the rectangle 
		 * object, x and y are always casted to ints. This is so because there aren't halved pixels
		 * and it leads to all kind of funny artifacts if you try to get that done (until you have
		 * casted everything to int so it works again - yay).
		 * 
		 * Instead, look at the _panPoint var.
		 */
		protected var _offset:Rectangle = new Rectangle();
		/**
		 * The _panPoint variable is the "high precision" offset point. This is used to allow the 
		 * user to pan by subpixel coordinates. E.g. if you pan by .5,0 two times, the scene should
		 * jump to the next pixel.
		 */
		protected var _panPoint:Point = new Point();
		protected var _scaling:Number = 1;
		
		public function BlitSpriteScene(width:int = 100, height:int = 100, transparent: Boolean = false) {
			var mcnt:int = MAX_SIZE / CELL_SIZE;
			for (var i:int = 0; i < mcnt * mcnt; i += 1) _dirtPool.push(new Rectangle()); // dirt pool must have
				// enough rectangles for each cell as it's being used as temp vars
			_isTransparent = transparent;
			setDimension(width, height);
		}
		
		public function get width ():int { return _bm.width }
		public function get height ():int { return _bm.height }
		
		public function get scaling ():Number {
			return _scaling;
		}
		public function set scaling (scale:Number):void {
			_scaling = scale;
			markArea(_offset.x, _offset.y, _offset.width / scale, _offset.height / scale);
		}
		/**
		 * Moves the view to the given location. Automatically marks the areas to 
		 * be redrawn, moves the pixels on the buffer accordingly. You'll need
		 * to call the updateImage method to get an updated view.
		 * 
		 * @param	x pixels non scaled
		 * @param	y pixels non scaled
		 * @return self
		 */
		public function panTo (hix:Number, hiy:Number):BlitSpriteScene {
			if (_panPoint.x == hix && _panPoint.y == hiy) return this;
			_panPoint.x = hix;
			_panPoint.y = hiy;
			var x:int = int(hix);
			var y:int = int(hiy);
			var dx:int = x - _offset.x;
			var dy:int = y - _offset.y;
			_bm.scroll(-dx,  -dy);
			_offset.x = x;
			_offset.y = y;
			if (_scaling != 1) {
				markArea(_offset.x, _offset.y, _offset.width / _scaling, _offset.height/_scaling);
			}
			if (dx <= 0) {
				if (dx<0) markArea(x, y, -dx, _offset.height / scaling);
				if (dy < 0) {
					markArea(x - dx, y, _offset.width / scaling + dx, -dy);
					//trace("->", y);
					/*for (var i:int = 0; i < _dirtListUse; i += 1) {
						trace("D",i,_dirtPool[i]);
					}*/
				} else if(dy>0) {
					markArea(x - dx, y+_offset.height-dy, _offset.width / _scaling + dx, dy);
				}
			} else if (dx > 0) {
				markArea(x + _offset.width - dx, y, dx, _offset.height / _scaling);
				if (dy < 0) {
					markArea(x, y, _offset.width - dx, -dy);
					//trace(x, y, _offset.width - dx, dy);
				} else if(dy>0) {
					markArea(x, y+_offset.height-dy, _offset.width / _scaling - dx, dy);
				}
			}
			return this;
		}
		
		/**
		 * Pans the view by the given amount of pixels (non scaled). Calls panTo.
		 * @param	x pixels
		 * @param	y pixels
		 * @return self
		 */
		public function panBy (x:Number, y:Number):BlitSpriteScene {
			return panTo(_panPoint.x + x, _panPoint.y + y);
		}
		
		/**
		 * Clears the list of marked areas for repaint. Be sure you know why you call it.
		 * You don't have to call it at all.
		 * @return self
		 */
		public function clearDirt():BlitSpriteScene {
			_dirtListUse = 0;
			return this;
		}
		
		/**
		 * Changes (if different size) the size of the bitmap. A new bitmap
		 * is created and must be reasigned wherever it was used. 
		 * @param	width 	pixel size
		 * @param	height	pixel size
		 * @return	self
		 */
		public function setDimension(width:int, height:int):BlitSpriteScene {
			if (_offset.width == width && _offset.height == height) return this;
			if (_bm) _bm.dispose();
			_bm = new BitmapData(_offset.width = width, _offset.height = height,_isTransparent,0);
			return this.clearDirt().markArea(_offset.x,_offset.y,width/_scaling,height/_scaling);
		}
		
		public function set isTransparent (on:Boolean):void {
			if (on != _isTransparent) {
				_isTransparent = on;
				setDimension(_bm.width, _bm.height);
			}
		}
		
		public function get isTransparent (): Boolean {
			return _isTransparent;
		}
		
		/**
		 * the bitmapdata object reference. Changes when the dimension is changed. Be aware of this.
		 */
		public function get bitmapData ():BitmapData {
			return _bm;
		}
		
		private var _lsobjects:Vector.<SpaceObject> = new Vector.<SpaceObject>();
		private var _lsobjectsall:Vector.<SpaceObject> = new Vector.<SpaceObject>();
		private var _lsrects:Vector.<Rectangle> = new Vector.<Rectangle>();
		private var _tmprect:Rectangle = new Rectangle();
		private var _tmpcliprect:Rectangle = new Rectangle();
		private var _stats:Dictionary = new Dictionary();
		/**
		 * Updates the image so that all marked areas are repainted. If nothing has been
		 * changed, nothing is painted. Clears the list of marked areas.
		 * 
		 * @return self
		 */
		public function updateImage():BlitSpriteScene {
			// beware, this function is huge since it takes care of a couple sequentially executed steps
			// Using some class variables here to avoid dynamic object allocations, so some stuff needs initializations
			_tmprect.width = _tmprect.height = 0;
			var _dirtListUse:int = this._dirtListUse;
			this._dirtListUse = 0;
			
			var minX:Number = NaN, maxX:Number, minY:Number, maxY:Number;
			// get an overview how many rectangles we have in view. This is done using a screen array that is 
			// of fixed size. We can point into it using the good old x+y*width formula and look up these 
			// integer values stored there. There's a ditmarker around that is increased as long as the application runs.
			// By using the int values, we can avoid resetting the field values back between the runs.
			var screenidx:int,x:int,y:int;
			_dirtmarker += 4; // Any field marked with this number or higher is dirty. Omitting some numbers because 
				// these are used in between and I don't want to reset the cells
			for (var i:int = _dirtListUse-1; i >= 0; i -= 1) {
				// this loop rasterizes all the dirt rectangles we have - it updates the screen cell's to the 
				// dirtmarker value, indicating that this cell requires an update.
				var r:Rectangle = _dirtPool[i];
				r.x = (r.x-_offset.x)*_scaling+_offset.x;
				r.y = (r.y-_offset.y)*_scaling+_offset.y;
				r.width *= _scaling;
				r.height *= _scaling;
				var x1:int = Math.max(0, Math.floor((r.x - _offset.x) / CELL_SIZE));
				var y1:int = Math.max(0, Math.floor((r.y - _offset.y) / CELL_SIZE));
				var x2:int = Math.min(MAX_SIZE/CELL_SIZE-1, Math.floor((r.x + r.width - _offset.x) / CELL_SIZE));
				var y2:int = Math.min(MAX_SIZE/CELL_SIZE-1, Math.floor((r.y + r.height - _offset.y) / CELL_SIZE));
				for (y = y1; y <= y2; y += 1) {
					screenidx = y * (MAX_SIZE / CELL_SIZE) + x1;
					for (x = x1; x <= x2; x += 1, screenidx+=1) {
						_dirtyScreenArea[screenidx] = _dirtmarker;
					}
				}
			}
			
			// fill smaller gaps - we have now a "holy" mess and we want to fill up smaller gaps in favor of creating
			// larger rectangles. A gap is filled if either the left and right or the top and bottom neighbor is 
			// dirty. The indication is made by setting the cell's value to _dirtmarker + 1, so we won't fill gaps 
			// in recursion when running through the loop. 
			for (y = 0; y <= _offset.height / CELL_SIZE; y += 1) {
				screenidx = y * (MAX_SIZE / CELL_SIZE);
				for (x = 0; x <= _offset.width / CELL_SIZE; x += 1, screenidx += 1) {
					if (_dirtyScreenArea[screenidx] == _dirtmarker) continue; // ignore dirty ones
					if (((x > 0 && _dirtyScreenArea[screenidx - 1] == _dirtmarker) && 
						 (x < _offset.width / CELL_SIZE && _dirtyScreenArea[screenidx + 1] == _dirtmarker)) ||
						((y > 0 && _dirtyScreenArea[screenidx - MAX_SIZE / CELL_SIZE] == _dirtmarker) &&
						 (y < _offset.height / CELL_SIZE && _dirtyScreenArea[screenidx + MAX_SIZE / CELL_SIZE] == _dirtmarker))) 
						_dirtyScreenArea[screenidx] = _dirtmarker + 1; // mark the one with a higher value so we won't 
							// interact with it in this loop
				}
			}
			
			// now we need to create rectangles from the rasterized grid again, so first let's reset our object's 
			// local storage array
			_lsrects.length = 0;
			var currentRect:Rectangle = null,useidx:int = 0;
			for (y = 0; y <= _offset.height / CELL_SIZE; y += 1) {
				screenidx = y * (MAX_SIZE / CELL_SIZE);
				currentRect = null;// the current rect is the rect we append stuff to if the new rect fits next to it
					// (the algorithm scans from left to right)
				for (x = 0; x <= _offset.width / CELL_SIZE; x += 1, screenidx += 1) {
					if (_dirtyScreenArea[screenidx] >= _dirtmarker) {
						var hscan:int, rh:int = 1;
						var hidx :int = screenidx + (MAX_SIZE / CELL_SIZE);
						// scanning now till the bottom the sequence of connected cells. We reset the cell value so we
						// won't use the same cell for a rectangle again.
						for (hscan = y + 1; hscan <= _offset.height / CELL_SIZE ; hscan += 1, hidx+=MAX_SIZE/CELL_SIZE) {
							if (_dirtyScreenArea[hidx] >= _dirtmarker) {
								rh += 1;
								_dirtyScreenArea[hidx] = 0;
								continue;
							}
							break;
						}
						x1 = x * CELL_SIZE;
						y1 = y * CELL_SIZE;
						if (!currentRect || (currentRect.x +currentRect.width != x1) || (currentRect.height != rh * CELL_SIZE)) {
							currentRect = _dirtPool[useidx += 1]; 
							_lsrects.push(currentRect);	
							_setRect(currentRect, x1, y1, CELL_SIZE, rh*CELL_SIZE)
						} else {
							currentRect.width += CELL_SIZE; // same height, next to it - just extend the currently used rectangle
						}
						
					}
				}
			}
			
			if (_lsrects.length == 0) {
				return this; // no need to bother anymore...
			}
			
			// Our spaces are going to be queried only once, not individually per rectangle. I might be wrong
			// about this decission, but the idea is, that making a query to a space is fairly expensive. 
			// A larger region query can be still filtered quite efficiently afterwards. The sorting is 
			// also expensive, so maybe this belongs here:
			// TODO: Test if individual space request are improving the efficiency.
			// apart from that: the next loop is determining the largest rectangle on the screen that fits all the dirt around.
			//var dbg:Number = -1;
			for (i = 0; i < _lsrects.length; i += 1) {
				r = _lsrects[i];
				r.x = (r.x) / _scaling + _offset.x;
				r.y = (r.y) / _scaling + _offset.y;
				r.width /= _scaling;
				r.height /= _scaling;
				if (isNaN(minX)) {
					minX = Math.max(_offset.x, r.x);
					maxX = Math.min(_offset.x + _offset.width/_scaling, r.x + r.width);
					minY = Math.max(_offset.y, r.y);
					maxY = Math.min(_offset.y + _offset.height/_scaling, r.y + r.height);
					continue;
				}
				var t:Number;
				if (minX > (t = Math.max(r.x, _offset.x))) minX = t;
				if (maxX < (t = Math.min(r.x + r.width, _offset.x + _offset.width/_scaling))) maxX = t;
				if (minY > (t = Math.max(r.y, _offset.y))) minY = t;
				if (maxY < (t = Math.min(r.y + r.height, _offset.y + _offset.height/_scaling))) maxY = t;
				
			}
			
			// some more initializations for upcoming loopings
			_setRect(_tmprect, minX, minY, (maxX - minX), (maxY - minY));
			_stats.drawcalls = 0;
			_stats.objcount = 0;
			_lsobjectsall.length = 0;
			var bso:SpaceObject;
			// we'll query all spaces in the following loop, sort the results and append it to our buffer
			for each (var space:Space in _sceneSpaces) {
				_lsobjects.length = 0;
				
				// find now all the objects we are cutting with EACH of our dirty rectangles
				space.enlistContent(_tmprect, _lsobjects);
				if (_lsobjects.length == 0) continue;
				// the sorting is quite expensive, so let's only do it for all of the objects in the
				// dirty area and sort out matching sprites on the go.
				_lsobjects.sort(sortFunc);
				_stats.objcount += _lsobjects.length;
				// append the objects to the list - in order of drawing
				for each (bso in _lsobjects) _lsobjectsall.push(bso);
			}
			
			// finally, we DRAW the stuff on the screen. 
			_bm.lock();
			for each (r in _lsrects) {
				_setRect(_tmpcliprect, (r.x - _offset.x)*_scaling, (r.y - _offset.y)*_scaling, r.width*_scaling, r.height*_scaling);
				for each (bso in _lsobjectsall) {
					if (!bso.intersects(r)) continue;
					++_stats.drawcalls;
					bso.draw(_bm, (bso.x - _offset.x)*_scaling, (bso.y - _offset.y)*scaling, _scaling, _tmpcliprect);
				}
			}
			_bm.unlock(_tmprect);
			return this;
		}
		
		protected function sortFunc (a:SpaceObject, b:SpaceObject):int {
			var sa:Number = a.sortIndex, sb:Number = b.sortIndex;
			return (sa == sb? (a.uid < b.uid ? -1 : 1) : (sa < sb? -1:1));
		}
		
		/**
		 * Appends all objects found at x,y into appendTo. Remember to empty the vector if it's
		 * being reused. The objects appended to the apendTo list are sorted by spaceobject order
		 * within their spaces. There is no sorting happening between objects from different 
		 * spaces. The within-list spaces are traversed in the order as they are passed - it 
		 * may differ from the order within the scene on purpose. The last object in the
		 * appendTo list is the topmost one. All objects found at one location are returned.
		 * 
		 * @param	x
		 * @param	y
		 * @param	appendTo
		 * @param	within optional list of spaces that should be checked, order matters, if null, 
		 * 			all spaces of the scene are being queried
		 * @return appendTo
		 */
		public function getObjectsAt (x:Number, y:Number, appendTo:Vector.<SpaceObject>, within: Vector.<Space> = null):Vector.<SpaceObject> {
			x = x / _scaling + _offset.x;
			y = y / _scaling + _offset.y;
			for each (var space:Space in (within || _sceneSpaces)) {
				_lsobjects.length = 0;
				space.getObjectsAt(x, y, _lsobjects);
				_lsobjects.sort(sortFunc);
				for each (var so:SpaceObject in _lsobjects) {
					appendTo.push(so);
				}
			}
			return appendTo;
		}
		
		/**
		 * How many objects have been considered for updating on last updateImage call?
		 */
		public function get statsVisibleObjectCount ():int {
			return int(_stats.objcount);
		}
		
		/**
		 * How many calls on the draw method of the objects have been called on last updateImage call?
		 * The number can be (much) smaller than the number of considered images as each
		 * flagged rectangle is tested against each object again.
		 */
		public function get statsDrawCalls ():int {
			return int(_stats.drawcalls);
		}
		
		/**
		 * The total number of objects in all spaces.
		 */
		public function get objectCount ():int {
			var i:int = 0;
			for each (var space:Space in _sceneSpaces) i += space.totalContentCount;
			return i;
		}
		
		/**
		 * Appends a new space to the List of spaces. There's right now no ordering, so you should
		 * set it up in the correct order. This is in concordance with the usage of this object:
		 * A change of spaces during usage shouldn't be required. If you want to do that, you might
		 * want to reconsider it and use a different approach as this is not a plausible way to deal
		 * with this class. Maybe you need to learn more.
		 * 
		 * @param	space The space to be added. Will drawn on top of spaces that were added earlier.
		 * @return self
		 */
		public function append(space:Space):BlitSpriteScene {
			if (space.owner == this) return this;
			space.owner = this;
			_sceneSpaces.push(space);
			return markArea(space.x, space.y, space.width, space.height);
		}
		
		/**
		 * Flags a rectangular area to be dirty and it will be scheduled for redrawing. The next
		 * updateImage call will take care of redrawing these areas accordingly (if it's in visible
		 * scope)
		 * @param	x
		 * @param	y
		 * @param	width
		 * @param	height
		 * @return
		 */
		public function markArea(x:int, y:int, width:int, height:int):BlitSpriteScene {			
			var i:int;
			if (_dirtListUse == _dirtPool.length) {
				// avoid dynamic allocations, so let's double our number of available rectangles
				for (i = Math.max(1,_dirtListUse); i >= 0; i -= 1) {
					_dirtPool.push(new Rectangle());
				}
			}
			var r:Rectangle = _setRect(_dirtPool[_dirtListUse++], x, y, width, height);
			return this;
		}
		
		/**
		 * Calls the animationTick method of all objects that are visible right now. 
		 * Ommits spaces and objects that are not flagged for animation callbacks.
		 * 
		 * @param	t 	number parameter for animation time. Passed as it is to the 
		 * 				animationTick method of the objects. Blitwit is using milliseconds
		 * 				received from flash.utils.getTimer(), but you may also come
		 * 				with your own stuff if you don't use the stock SpaceObjects.
		 * @param	data Variable optional parameter to be passed as well.
		 * @return self
		 */
		public function animationTick (t:Number, data:* = null):BlitSpriteScene {
			_lsobjects.length = 0;
			_setRect(_tmprect, _offset.x, _offset.y, _offset.width / _scaling, _offset.height / _scaling);
			for each (var space:Space in _sceneSpaces) {
				if (space.isAnimated)
					space.enlistContent(_tmprect, _lsobjects);
			}
			for each (var sprite:SpaceObject in _lsobjects) {
				if (sprite.isAnimated) sprite.animationTick(this, t, data);
			}
			return this;
		}
		
		private function _swapRect(a:int, b:int):BlitSpriteScene {
			if (a == b) return this;
			var t:Rectangle = _dirtPool[a];
			_dirtPool[a] = _dirtPool[b];
			_dirtPool[b] = t;
			return this;
		}
		
		private function _setRect(r:Rectangle, x:int, y:int, width:int, height:int):Rectangle {
			r.x = x;
			r.y = y;
			r.width = width;
			r.height = height;
			return r;
		}
		
	}

}